package com.zj.binding;

import com.zj.annotations.Param;
import com.zj.exception.BindingException;
import com.zj.mapping.MappedStatement;
import com.zj.mapping.SqlCommandType;
import com.zj.sqlSession.ResultHandler;
import com.zj.sqlSession.SqlSession;
import com.zj.utils.Configuration;

import java.lang.reflect.Method;
import java.util.*;

public class MapperMethod {
    private final SqlSession sqlSession;
    private final Configuration config;
    private SqlCommandType type;
    private String commandName;

    private Class<?> declaringInterface;
    private Method method;

    private boolean returnsMany;
    private boolean returnsMap;
    private boolean returnsVoid;

    private List<String> paramNames;
    private List<Integer> paramPositions;

    private boolean hasNamedParameters;

    public MapperMethod(Class<?> declaringInterface, Method method, SqlSession sqlSession) {
        paramNames = new ArrayList<String>();
        paramPositions = new ArrayList<Integer>();
        this.sqlSession = sqlSession;
        this.method = method;
        this.config = sqlSession.getConfiguration();

        this.declaringInterface = declaringInterface;

        this.commandName = declaringInterface.getName() + "." + method.getName();;
        setupMethodSignature();
        setupCommandType();

    }

    private void setupMethodSignature() {
        if (method.getReturnType().equals(Void.TYPE)) {
            returnsVoid = true;
        }
        if ( Collection.class.isAssignableFrom(method.getReturnType()) || method.getReturnType().isArray()) {
            returnsMany = true;
        }
        if (Map.class.isAssignableFrom(method.getReturnType())) {
            returnsMap = true;
        }
        final Class<?>[] argTypes = method.getParameterTypes();
        for (int i = 0; i < argTypes.length; i++) {
            String paramName = String.valueOf(paramPositions.size());
            paramName = getParamNameFromAnnotation(i, paramName);
            paramNames.add(paramName);
            paramPositions.add(i);
        }
    }

    private String getParamNameFromAnnotation(int i, String paramName) {
        Object[] paramAnnos = method.getParameterAnnotations()[i];
        for (int j = 0; j < paramAnnos.length; j++) {
            if (paramAnnos[j] instanceof Param) {
                hasNamedParameters = true;
                paramName = ((Param) paramAnnos[j]).value();
            }
        }
        return paramName;
    }

    private void setupCommandType() {
        MappedStatement ms = config.getMappedStatement(commandName);
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + commandName);
        }
    }

    public Object execute(Object[] args) {
        Object result = null;
        if (SqlCommandType.INSERT == type) {
            Object param = getParam(args);
            result = sqlSession.insert(commandName, param);
        } else if (SqlCommandType.UPDATE == type) {
            Object param = getParam(args);
            result = sqlSession.update(commandName, param);
        } else if (SqlCommandType.DELETE == type) {
            Object param = getParam(args);
            result = sqlSession.delete(commandName, param);
        } else if (SqlCommandType.SELECT == type) {
            if (returnsMany) {
                result = executeForMany(args);
            } else if (returnsMap) {
//                result = executeForMap(args);
            } else {
                Object param = getParam(args);
                result = sqlSession.selectOne(commandName, param);
            }
        } else {
            throw new BindingException("Unknown execution method for: " + commandName);
        }
        return result;
    }

    private Object getParam(Object[] args) {
        final int paramCount = paramPositions.size();
        if (args == null || paramCount == 0) {
            return null;
//        } else if (!hasNamedParameters && paramCount == 1) {
//            return args[paramPositions.get(0)];
        } else {
            Map<String, Object> param = new HashMap<>();
            for (int i = 0; i < paramCount; i++) {
                param.put(paramNames.get(i), args[paramPositions.get(i)]);
            }
            // issue #71, add param names as param1, param2...but ensure backward compatibility
            for (int i = 0; i < paramCount; i++) {
                String genericParamName = "param" + String.valueOf(i + 1);
                if (!param.containsKey(genericParamName)) {
                    param.put(genericParamName, args[paramPositions.get(i)]);
                }
            }
            return param;
        }
    }

//    private <K, V> Map<K, V> executeForMap(Object[] args) {
//        Map<K, V> result;
//        Object param = getParam(args);
//        result = sqlSession.<K, V>selectMap(commandName, param, mapKey);
//        return result;
//    }

    private <E> Object executeForMany(Object[] args) {
        List<E> result;
        Object param = getParam(args);
        result = sqlSession.<E>selectList(commandName, param);
        return result;
    }
}
